/*
 * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.oracle.svm.hosted.webimage.wasmgc.codegen;

import static jdk.graal.compiler.core.common.spi.ForeignCallDescriptor.CallSideEffect.NO_SIDE_EFFECT;

import java.util.Map;

import org.graalvm.word.LocationIdentity;

import com.oracle.svm.core.graal.jdk.SubstrateObjectCloneNode;
import com.oracle.svm.core.graal.jdk.SubstrateObjectCloneWithExceptionNode;
import com.oracle.svm.core.graal.meta.SubstrateForeignCallsProvider;
import com.oracle.svm.core.graal.snippets.NodeLoweringProvider;
import com.oracle.svm.core.snippets.SnippetRuntime;
import com.oracle.svm.core.snippets.SubstrateForeignCallTarget;
import com.oracle.svm.hosted.meta.HostedClass;
import com.oracle.svm.hosted.meta.HostedField;
import com.oracle.svm.hosted.meta.HostedInstanceClass;
import com.oracle.svm.hosted.meta.HostedType;
import com.oracle.svm.hosted.webimage.wasm.ast.Function;
import com.oracle.svm.hosted.webimage.wasm.ast.Instruction;
import com.oracle.svm.hosted.webimage.wasm.ast.Instruction.Const;
import com.oracle.svm.hosted.webimage.wasm.ast.Instructions;
import com.oracle.svm.hosted.webimage.wasm.ast.TypeUse;
import com.oracle.svm.hosted.webimage.wasm.ast.id.WasmId;
import com.oracle.svm.hosted.webimage.wasm.ast.id.WasmIdFactory;
import com.oracle.svm.hosted.webimage.wasm.codegen.WasmFunctionTemplate;
import com.oracle.svm.hosted.webimage.wasmgc.ast.id.GCKnownIds;
import com.oracle.svm.hosted.webimage.wasmgc.ast.id.WebImageWasmGCIds;
import com.oracle.svm.hosted.webimage.wasmgc.ast.visitors.WasmGCElementCreator;
import com.oracle.svm.hosted.webimage.wasmgc.types.WasmGCUtil;
import com.oracle.svm.hosted.webimage.wasmgc.types.WasmRefType;
import com.oracle.svm.webimage.wasm.WasmForeignCallDescriptor;
import com.oracle.svm.webimage.wasm.types.WasmPrimitiveType;
import com.oracle.svm.webimage.wasm.types.WasmUtil;

import jdk.graal.compiler.core.common.spi.ForeignCallDescriptor;
import jdk.graal.compiler.graph.Node;
import jdk.graal.compiler.nodes.NodeView;
import jdk.graal.compiler.nodes.StructuredGraph;
import jdk.graal.compiler.nodes.extended.ForeignCallNode;
import jdk.graal.compiler.nodes.extended.ForeignCallWithExceptionNode;
import jdk.graal.compiler.nodes.spi.LoweringTool;
import jdk.vm.ci.meta.JavaKind;
import jdk.vm.ci.meta.ResolvedJavaField;

/**
 * Function templates to support cloning in the WasmGC backend.
 *
 * @see WasmFunctionTemplate
 */
public class WasmGCCloneSupport {

    public static final SnippetRuntime.SubstrateForeignCallDescriptor CLONE = SnippetRuntime.findForeignCall(WasmGCCloneSupport.class, "doClone", NO_SIDE_EFFECT, LocationIdentity.any());
    private static final SnippetRuntime.SubstrateForeignCallDescriptor[] FOREIGN_CALLS = new SnippetRuntime.SubstrateForeignCallDescriptor[]{CLONE};

    /**
     * Calls to this are linked to {@link GCKnownIds#genericCloneTemplate} during codegen.
     */
    public static final WasmForeignCallDescriptor CLONE_TEMPLATE = new WasmForeignCallDescriptor("clone", Object.class, new Class<?>[]{Object.class});

    public static void registerForeignCalls(SubstrateForeignCallsProvider foreignCalls) {
        foreignCalls.register(FOREIGN_CALLS);
    }

    public static void registerLowerings(Map<Class<? extends Node>, NodeLoweringProvider<?>> lowerings) {
        lowerings.put(SubstrateObjectCloneNode.class, new ObjectCloneLowering());
        lowerings.put(SubstrateObjectCloneWithExceptionNode.class, new ObjectCloneWithExceptionLowering());
    }

    /**
     * Whether this type needs a clone function generated by {@link ObjectCloneTemplate}.
     * <p>
     * Only types determined to be cloneable by the analysis need this.
     */
    public static boolean needsCloneTemplate(HostedType type) {
        return type.isCloneableWithAllocation();
    }

    /**
     * The function signature of the {@link GCKnownIds#cloneFieldType}.
     */
    public static TypeUse getCloneFieldTypeUse(WasmGCUtil util) {
        WasmId.StructType javaLangObjectId = util.getJavaLangObjectId();
        return TypeUse.forUnary(javaLangObjectId.asNonNull(), javaLangObjectId.asNullable());
    }

    @SubstrateForeignCallTarget(stubCallingConvention = false)
    private static Object doClone(Object original) throws CloneNotSupportedException {
        if (original == null) {
            throw new NullPointerException();
        } else if (!(original instanceof Cloneable)) {
            throw new CloneNotSupportedException("Object is no instance of Cloneable.");
        }

        return callCloneTemplate(CLONE_TEMPLATE, original);
    }

    @Node.NodeIntrinsic(ForeignCallNode.class)
    private static native Object callCloneTemplate(@Node.ConstantNodeParameter ForeignCallDescriptor descriptor, Object original);

    static final class ObjectCloneLowering implements NodeLoweringProvider<SubstrateObjectCloneNode> {
        @Override
        public void lower(SubstrateObjectCloneNode node, LoweringTool tool) {
            StructuredGraph graph = node.graph();

            ForeignCallNode call = graph.add(new ForeignCallNode(CLONE, node.getObject()));
            call.setBci(node.bci());
            call.setStamp(node.stamp(NodeView.DEFAULT));
            graph.replaceFixedWithFixed(node, call);
        }
    }

    static final class ObjectCloneWithExceptionLowering implements NodeLoweringProvider<SubstrateObjectCloneWithExceptionNode> {
        @Override
        public void lower(SubstrateObjectCloneWithExceptionNode node, LoweringTool tool) {
            StructuredGraph graph = node.graph();

            ForeignCallWithExceptionNode call = graph.add(new ForeignCallWithExceptionNode(CLONE, node.getObject()));
            call.setBci(node.bci());
            call.setStamp(node.stamp(NodeView.DEFAULT));
            graph.replaceWithExceptionSplit(node, call);
        }
    }

    /**
     * Function that clones an arbitrary object by reading the hub's {@link GCKnownIds#cloneField}
     * and calling the function.
     *
     * <pre>{@code
     *  (func $func.clone.object (param $obj (ref null $_Object)) (result (ref $_Object))
     *    (call_ref $func.clone
     *      (local.get $obj)
     *      (struct.get $Ljava_lang_Class_ $field.clone
     *        (struct.get $_Object $field.dynamicHub
     *          (local.get $obj)
     *        )
     *      )
     *    )
     *  )
     * }</pre>
     */
    public static class GenericCloneTemplate extends WasmFunctionTemplate.Singleton {

        public GenericCloneTemplate(WasmIdFactory idFactory) {
            super(idFactory);
        }

        @Override
        protected String getFunctionName() {
            return "clone.object";
        }

        @Override
        protected Function createFunction(Context context) {
            WebImageWasmGCProviders providers = (WebImageWasmGCProviders) context.getProviders();
            WasmGCUtil util = providers.util();
            GCKnownIds knownIds = providers.knownIds();

            Function f = context.createFunction(getCloneFieldTypeUse(util), "Clones an arbitrary cloneable object");
            WasmId.Local objectParam = f.getParam(0);

            f.getInstructions().add(new Instruction.CallRef(
                            knownIds.cloneFieldType,
                            new Instruction.StructGet(
                                            util.getHubObjectId(),
                                            knownIds.cloneField,
                                            WasmUtil.Extension.None,
                                            providers.builder().getHub(objectParam.getter())),
                            Instructions.asInstructions(objectParam.getter())));

            return f;
        }
    }

    /**
     * Creates a clone function for a specific instance type.
     * <p>
     * The cloning is simply a {@code struct.new} instruction containing all the field values of the
     * original object with the identity hash code set to 0 (which is the marker value for it not
     * being computed yet).
     * <p>
     * References to these function are stored in {@link GCKnownIds#cloneField} for any cloneable
     * instance type (see {@link #needsCloneTemplate(HostedType)})
     * <p>
     * Generates approximately:
     *
     * <pre>{@code
     * (func $func.clone.object.<type> (param $obj (ref null $_Object)) (result (ref $_Object))
     *   (local $cast_obj (ref $<type>))
     *   (local.set $cast_obj
     *     (ref.cast (ref $<type>)
     *       (local.get $obj)
     *     )
     *   )
     *   (struct.new $<type>
     *     (struct.get $<type> $field.dynamicHub
     *       (local.get $cast_obj)
     *     )
     *     (i32.const 0x0) (; Identity hash code ;)
     *     (struct.get $<type> $field1
     *       (local.get $cast_obj)
     *     )
     *     (struct.get $<type> $field2
     *       (local.get $cast_obj)
     *     )
     *     (; Load other fields... ;)
     *   )
     * )
     * }</pre>
     */
    public static class ObjectCloneTemplate extends WasmFunctionTemplate<HostedInstanceClass> {

        public ObjectCloneTemplate(WasmIdFactory idFactory) {
            super(idFactory, true);
        }

        @Override
        protected String getFunctionName(HostedInstanceClass clazz) {
            return "clone.object." + clazz.getJavaClass().getTypeName();
        }

        @Override
        protected Function createFunction(Context context) {
            WebImageWasmGCProviders providers = (WebImageWasmGCProviders) context.getProviders();
            GCKnownIds knownIds = providers.knownIds();
            WasmGCUtil util = providers.util();
            HostedClass type = context.getParameter();
            WebImageWasmGCIds.JavaStruct struct = idFactory.newJavaStruct(type);
            WasmRefType wasmType = struct.asNonNull();

            Function f = Function.create(idFactory, context.getId(), knownIds.cloneFieldType, getCloneFieldTypeUse(util), "Clones object of type " + type.getJavaClass().getTypeName());
            WasmId.Local objectRef = idFactory.newTemporaryVariable(wasmType);

            f.getInstructions().add(objectRef.setter(new Instruction.RefCast(f.getParam(0).getter(), wasmType)));

            Instructions fields = new Instructions();

            fields.add(new Instruction.StructGet(struct, knownIds.hubField, WasmUtil.Extension.None, objectRef.getter()));
            // Identity hash code. A value of 0 indicates that it was not yet computed
            fields.add(Const.forInt(0));

            for (ResolvedJavaField field : WasmGCElementCreator.getInstanceFields(type)) {
                HostedField hField = (HostedField) field;
                WebImageWasmGCIds.JavaField javaField = idFactory.newJavaField(field);
                fields.add(new Instruction.StructGet(struct, javaField, WasmUtil.Extension.forKind(hField.getStorageKind()), objectRef.getter()));
            }

            f.getInstructions().add(new Instruction.StructNew(struct, fields.toArray()));
            return f;
        }
    }

    /**
     * Creates an array clone function parameterized on the component kind.
     * <p>
     * First allocates an empty new array and then calls {@link GCKnownIds#arrayCopyTemplate} to
     * copy over array elements:
     * <p>
     * References to these function are stored in {@link GCKnownIds#cloneField} for any cloneable
     * array type (see {@link #needsCloneTemplate(HostedType)})
     * <p>
     *
     * <pre>{@code
     * (func $func.clone.array.<component kind> (param $obj (ref null $_Object)) (result (ref $_Object))
     *   (local $old_array (ref $struct.array.<component kind>))
     *   (local $length i32)
     *   (local $new_array (ref $struct.array.<component kind>))
     *   (local.set $old_array
     *     (ref.cast (ref $struct.array.<component kind>)
     *       (local.get $obj)
     *     )
     *   )
     *   (local.set $length
     *     (array.len
     *       (struct.get $struct.baseArray $field.inner
     *         (local.get $old_array)
     *       )
     *     )
     *   )
     *   (; Allocate empty new array ;)
     *   (local.set $new_array
     *     (struct.new $struct.array.<component kind>
     *       (struct.get $_Object $field.dynamicHub
     *         (local.get $old_array)
     *       )
     *       (i32.const 0x0)
     *       (array.new_default $array.<component kind>
     *         (local.get $length)
     *       )
     *     )
     *   )
     *   (; Call to {@link GCKnownIds#arrayCopyTemplate} to copy array contents ;)
     *   (call $func.arraycopy.<component kind>
     *     (local.get $old_array)
     *     (i32.const 0x0)
     *     (local.get $new_array)
     *     (i32.const 0x0)
     *     (local.get $length)
     *   )
     *   (return
     *     (local.get $new_array)
     *   )
     * )
     * }</pre>
     */
    public static class ArrayCloneTemplate extends WasmFunctionTemplate<JavaKind> {

        public ArrayCloneTemplate(WasmIdFactory idFactory) {
            super(idFactory, true);
        }

        @Override
        protected boolean isValidParameter(JavaKind componentKind) {
            return componentKind.getSlotCount() > 0;
        }

        @Override
        protected String getFunctionName(JavaKind componentKind) {
            return "clone.array." + componentKind;
        }

        @Override
        protected Function createFunction(Context context) {
            WebImageWasmGCProviders providers = (WebImageWasmGCProviders) context.getProviders();
            GCKnownIds knownIds = providers.knownIds();
            WasmGCUtil util = providers.util();
            JavaKind componentKind = context.getParameter();
            WasmId.StructType arrayStruct = idFactory.newJavaArrayStruct(componentKind);
            WasmRefType arrayStructType = arrayStruct.asNonNull();

            Function f = Function.create(idFactory, context.getId(), knownIds.cloneFieldType, getCloneFieldTypeUse(util), "Clones array of component kind " + componentKind);
            Instructions instructions = f.getInstructions();

            WasmId.Local arrayRef = idFactory.newTemporaryVariable(arrayStructType);
            WasmId.Local clonedArray = idFactory.newTemporaryVariable(arrayStructType);
            WasmId.Local arrayLength = idFactory.newTemporaryVariable(WasmPrimitiveType.i32);

            // Downcast Object parameter to array struct type
            instructions.add(arrayRef.setter(new Instruction.RefCast(f.getParam(0).getter(), arrayStructType)));
            instructions.add(arrayLength.setter(providers.builder.getArrayLength(arrayRef.getter())));
            // Allocate new array
            instructions.add(clonedArray
                            .setter(providers.builder().createNewArray(componentKind, providers.builder().getHub(arrayRef.getter()), arrayLength.getter())));
            // Copy array data
            instructions.add(new Instruction.Call(
                            knownIds.arrayCopyTemplate.requestFunctionId(componentKind),
                            arrayRef.getter(),
                            Const.forInt(0),
                            clonedArray.getter(),
                            Const.forInt(0),
                            arrayLength.getter()));

            instructions.add(new Instruction.Return(clonedArray.getter()));

            return f;
        }
    }
}
